const ccxt = require('ccxt')
const TI = require('trading-indicator')
const _ = require('lodash')
const Decimal = require('decimal.js')
const Service = require('./../service')
const moment = require('moment')
const service = require('./../service')
// 常量
const {
  OrderDirection,
  OrderDirectionDesc,
  LogType,
  StoreName: {
    DUAL_ORDER_INFO,
    OPEN_ORDER_INFO,
    LAST_LOSS_INFO,
    BOLL_STATUS_LIST
  },
  BollStatus,
  BollStatusChangeDesc,
  PositionSide
} = require('./../constants')

class AutoTrader {
  constructor (options = {}) {
    this.state = {
      exchangeId: options.exchangeId,
      apiKey: options.apiKey,
      secret: options.secret,
      tick: options.tick,
      symbol: options.tick.replace('/', ''),
      minBollWidthRatio: options.minBollWidthRatio,
      stochRsiUpper: options.stochRsiUpper,
      stochRsiLower: options.stochRsiLower,
      loopDuration: options.loopDuration,
      rsiUpper: options.rsiUpper,
      rsiLower: options.rsiLower,
      gapAfterLoss: options.minutesAfterLoss,
      transactionType: options.transactionType,
      isMock: options.isMock,
      levarage: options.levarage,
      buildRatio: options.buildRatio
    }
  }
  start () {
    this._login()
    this._startLoop()
  }
  stop () {
    clearInterval(this._interval)
  }
  _clear () {}
  _startLoop () {
    const { exchangeId, tick } = this.state
    console.log(`———— 开始监听${exchangeId}：${tick}行情 ————`)
    console.log(' ')
    this._interval = setInterval(() => {
      Service.clearFormatLog()
      this._clear()
      // 请求且判断
      Promise.all([
        this._requestBalance(),
        this._requestOpenOrders(),
        this._requestJudgeRSI(),
        this._requestJudgeBoll(),
        this._requestJudgeStochRSI()
      ]).then(
        ([
          {
            // balance,            // 总余额
            holdingAmount, // 持仓金额
            openOrderAmount, // 挂单金额
            availableBalance: freeAmount, // 可用余额（未下单的钱）
            isFree // 是否空仓
          },
          openOrders,
          {
            isRSIOverUp, // RSI是否超买
            isRSIOverDown, // RSI是否超卖
            rsi // rsi值
          },
          {
            isBreakUpBB, // 是否上穿布林线
            isBreakDownBB, // 是否下穿布林线
            price, // 当前价格
            bollWidth, // 布林半径
            bollUp, // 布林上轨
            bollMiddle, // 布林中轨
            bollDown, // 布林下轨
            lastBollItem // 上一项
          },
          {
            stochRSI, // stochRSI值
            d
          }
        ]) => {
          const {
            stochRsiUpper,
            stochRsiLower,
            minBollWidthRatio,
            shakeBuyOrder = {},
            shakeSellOrder = {}
            // isMock
          } = this.state

          Service.initFormatLog({
            bollUp,
            price,
            bollDown,
            bollWidth,
            minBollWidth: this._getMinBollWidth(price, minBollWidthRatio),
            rsi,
            stochRSI,
            d,
            xFactorBuy: shakeBuyOrder.xFactor,
            xFactorSell: shakeSellOrder.xFactor,
            bollWidthDiff: shakeSellOrder.bollWidthDif,
            bollUpDiff: shakeSellOrder.bollUpDiff,
            bollDownDiff: shakeSellOrder.bollDownDiff,
            lastBollItem
          })

          const dualOrderInfo = this.getStore(DUAL_ORDER_INFO)
          let openOrderInfo = this.getStore(OPEN_ORDER_INFO)
          // const lastLossInfo = this.getStore(LAST_LOSS_INFO)
          const { list: bollStatusList } = this.getStore(BOLL_STATUS_LIST)

          if (isFree) {
            this.setStore(OPEN_ORDER_INFO, null)
            this.setStore(DUAL_ORDER_INFO, null)
            // 兼容空仓时，止盈止损仍在的情况
            if (!holdingAmount && !openOrderAmount && openOrders.length) {
              this._cancelAllOrders()
              return
            }

            // 处理布林线过窄判断
            const bollStatus = this._getBollStatus({
              bollWidth,
              bollUp,
              bollMiddle,
              bollDown,
              price,
              bollStatusList
            })

            Service.addLogParams({
              bollStatus
            })

            Service.formatLog(
              {
                title: `空仓 - ${moment().format()}`,
                type: LogType.SUB
              },
              true
            )

            if (bollStatus === BollStatus.NORMAL) {
              /* 震荡行情逻辑 */
              // 兼容实际没开单，本地开了单的情况
              if (openOrderInfo) {
                this.setStore(OPEN_ORDER_INFO, null)
                this.setStore(DUAL_ORDER_INFO, null)
              }
              // 正常未开单下，开单条件监听
              if (
                typeof stochRSI === 'number' &&
                isFree && // 目前空仓
                this._isBollWidthNormal({
                  bollWidth,
                  price,
                  minBollWidthRatio
                })
              ) {
                /* 策略1 - 子策略监控 */
                if (isRSIOverUp) {
                  Service.formatLog({ title: 'RSI超买', type: LogType.SUB })
                }

                if (isBreakUpBB) {
                  Service.formatLog({ title: '布林线上穿', type: LogType.SUB })
                }

                if (stochRSI > stochRsiUpper) {
                  Service.formatLog({
                    title: 'stochRSI超买',
                    type: LogType.SUB
                  })
                }

                /* 策略2 - 子策略监控 */
                if (isRSIOverDown) {
                  Service.formatLog({ title: 'RSI超卖', type: LogType.SUB })
                }

                if (isBreakDownBB) {
                  Service.formatLog({ title: '布林线下穿', type: LogType.SUB })
                }

                if (stochRSI < stochRsiLower) {
                  Service.formatLog({
                    title: 'stochRSI超卖',
                    type: LogType.SUB
                  })
                }

                // 策略1
                if (
                  isRSIOverUp && // RSI超买
                  isBreakUpBB && // 布林线上穿
                  stochRSI > stochRsiUpper // stochRSI超买
                ) {
                  Service.formatLog({ title: '触及策略1', type: LogType.MAIN })
                  this._createOrder({
                    freeAmount,
                    price,
                    orderDirection: OrderDirection.SELL,
                    orderOpenPrice: shakeSellOrder.orderOpenPrice,
                    stopLossPrice: shakeSellOrder.stopLossPrice,
                    takeProfitPrice: shakeSellOrder.takeProfitPrice
                  })
                }

                // 策略2
                if (
                  isRSIOverDown && // RSI超卖
                  isBreakDownBB && // 布林线上穿
                  stochRSI < stochRsiLower // stochRSI超卖
                ) {
                  Service.formatLog({
                    title: '触及策略2',
                    type: LogType.MAIN,
                    orderDirection: OrderDirection.BUY
                  })

                  this._createOrder({
                    freeAmount,
                    price,
                    orderDirection: OrderDirection.BUY,
                    orderOpenPrice: shakeBuyOrder.orderOpenPrice,
                    stopLossPrice: shakeBuyOrder.stopLossPrice,
                    takeProfitPrice: shakeBuyOrder.takeProfitPrice
                  })
                }
              }
            } else if (bollStatus === BollStatus.ONESIDEUP) {
              // 确认进入单边上涨行情
              if (
                price >
                Decimal(bollUp)
                  .add(bollWidth * 0.5)
                  .toNumber()
              ) {
                // 1. 开单点：离布林上轨0.15半径位置(有点模糊，后面优化)
                const orderOpenPrice = Decimal(bollUp)
                  .sub(bollWidth * 0.15)
                  .toNumber()
                  .toFixed()
                // 止损点：布林中轨
                const stopLossPrice = bollMiddle
                // 止盈点：暂定价格1%(这里应该做成动态止盈，吃尽涨幅)
                const takeProfitPrice = Decimal(orderOpenPrice)
                  .add(price * 0.01)
                  .toNumber()
                  .toFixed()
                // 开单
                this._createOrder({
                  freeAmount,
                  price,
                  orderDirection: OrderDirection.BUY,
                  orderOpenPrice,
                  stopLossPrice,
                  takeProfitPrice
                })
              }
            } else if (bollStatus === BollStatus.ONESIDEDOWN) {
              // 确认进入单边下跌行情
              if (
                price <
                Decimal(bollDown)
                  .sub(bollWidth * 0.5)
                  .toNumber()
              ) {
                // 确认进入单边上涨行情
                // 1. 开单点：离布林上轨0.15半径位置(有点模糊，后面优化)
                const orderOpenPrice = Decimal(bollDown)
                  .add(bollWidth * 0.15)
                  .toNumber()
                  .toFixed()
                // 止损点：布林中轨
                const stopLossPrice = bollMiddle
                // 止盈点：暂定价格1%(这里应该做成动态止盈，吃尽涨幅)
                const takeProfitPrice = Decimal(orderOpenPrice)
                  .sub(price * 0.01)
                  .toNumber()
                  .toFixed()
                // 开单
                this._createOrder({
                  freeAmount,
                  price,
                  orderDirection: OrderDirection.SELL,
                  orderOpenPrice,
                  stopLossPrice,
                  takeProfitPrice
                })
              }
            }
          } else {
            Service.formatLog(
              {
                title: `挂单/持仓 - ${moment().format()}`,
                type: LogType.SUB
              },
              true
            )

            const {
              orderDirection,
              orderOpenPrice,
              stopLossPrice,
              takeProfitPrice
            } = shakeBuyOrder
            if (holdingAmount > 0 && !dualOrderInfo) {
              const side = orderDirection
              const sideDesc = OrderDirectionDesc[side]
              Service.formatOrderLog({
                title: `${sideDesc}成交`,
                type: LogType.ORDER,
                orderOpenPrice,
                stopLossPrice,
                takeProfitPrice
              })
              this.setStore(DUAL_ORDER_INFO, {
                dualPrice: orderOpenPrice,
                dualMoment: moment(),
                orderDirection: side
              })
            }
            if (holdingAmount) {
              // 已成交，是否触发止盈止损
              const { orderDirection } = shakeBuyOrder
              if (orderDirection === OrderDirection.SELL) {
                if (price >= stopLossPrice) {
                  // 触发止损
                  Service.formatOrderLog({
                    title: '触发空单止损',
                    type: LogType.ORDER,
                    orderOpenPrice,
                    stopLossPrice,
                    takeProfitPrice
                  })
                  this.setStore(DUAL_ORDER_INFO, null)
                  this.setStore(OPEN_ORDER_INFO, null)
                  this.setStore(LAST_LOSS_INFO, {
                    lastLostTimeStamp: moment().valueOf(),
                    orderDirection
                  })
                }

                if (price <= takeProfitPrice) {
                  // 触发止盈
                  Service.formatOrderLog({
                    title: '触发空单止盈',
                    type: LogType.ORDER,
                    orderOpenPrice,
                    stopLossPrice,
                    takeProfitPrice
                  })
                  this.setStore(DUAL_ORDER_INFO, null)
                  this.setStore(OPEN_ORDER_INFO, null)
                  this.setStore(LAST_LOSS_INFO, {
                    lastLostTimeStamp: moment().valueOf(),
                    orderDirection
                  })
                }
              }
              if (orderDirection === OrderDirection.BUY) {
                if (price <= stopLossPrice) {
                  // 触发止损
                  Service.formatOrderLog({
                    title: '触发多单止损',
                    type: LogType.ORDER,
                    orderOpenPrice,
                    stopLossPrice,
                    takeProfitPrice
                  })
                  this.setStore(DUAL_ORDER_INFO, null)
                  this.setStore(OPEN_ORDER_INFO, null)
                  this.setStore(LAST_LOSS_INFO, {
                    lastLostTimeStamp: moment().valueOf(),
                    orderDirection
                  })
                }

                if (price >= takeProfitPrice) {
                  // 触发止盈
                  Service.formatOrderLog({
                    title: '触发多单止盈',
                    type: LogType.ORDER,
                    orderOpenPrice,
                    stopLossPrice,
                    takeProfitPrice
                  })
                  this.setStore(DUAL_ORDER_INFO, null)
                  this.setStore(OPEN_ORDER_INFO, null)
                }
              }
            }

            // 有挂单、未开单
            if (!holdingAmount && !isFree && openOrders.length) {
              // 1. 回到布林轨道内 => 撤单
              if (
                price <
                  Decimal(bollUp)
                    .sub(bollWidth * 0.1)
                    .toNumber() &&
                price >
                  Decimal(bollDown)
                    .add(bollWidth * 0.1)
                    .toNumber()
              ) {
                this._cancelAllOrders()
              } else {
                Service.formatOrderLog({
                  title: '挂单中，未成交',
                  type: LogType.ORDER,
                  orderOpenPrice,
                  stopLossPrice,
                  takeProfitPrice
                })
              }
            }
          }
        }
      )
    }, this.state.loopDuration)
  }
  _login () {
    const exchangeId = this.state.exchangeId,
      exchangeClass = ccxt[exchangeId],
      exchange = new exchangeClass({
        apiKey: this.state.apiKey,
        secret: this.state.secret,
        password: '031612',
        timeout: 30000,
        enableRateLimit: true,
        options: {
          defaultType: this.state.transactionType,
          type: this.state.transactionType
        }
      })
    this.$exchange = exchange
    exchange.fapiPrivatePostLeverage({
      symbol: this.state.symbol,
      leverage: this.state.levarage
    })
  }
  _requestBalance () {
    return this.$exchange.fapiPrivateV2GetBalance().then(balanceInfo => {
      const usdtBalance = balanceInfo.find(item => item.asset === 'USDT')
      // 可用余额
      const {
        // 总余额
        balance,
        // 可用余额（未下单的钱）
        availableBalance,
        // 未持仓余额
        crossWalletBalance
      } = usdtBalance

      // 持仓金额
      const holdingAmount = Decimal(balance)
        .sub(crossWalletBalance)
        .toNumber()

      // 挂单金额
      const openOrderAmount = Decimal(crossWalletBalance)
        .sub(availableBalance)
        .toNumber()
      return {
        balance,
        holdingAmount,
        openOrderAmount,
        availableBalance,
        isFree: balance === availableBalance
      }
    })
  }
  _requestOpenOrders () {
    const { tick } = this.state
    return this.$exchange.fetchOpenOrders(tick)
  }
  _requestJudgeRSI () {
    const { exchangeId, rsiUpper, rsiLower, tick } = this.state
    return TI
      .rsiCheck(6, rsiUpper, rsiLower, exchangeId, tick, '3m', true)
      .then(rsiCheckRes => {
        const { overBought, overSold, rsiVal } = rsiCheckRes
        return {
          isRSIOverUp: overBought,
          isRSIOverDown: overSold,
          rsi: rsiVal
        }
      })
      .catch(err => {
        console.log(`【${exchangeId}】获取RSI超买/超卖判断失败`)
        console.log(err)
      })
  }
  _requestJudgeBoll () {
    const { exchangeId, tick } = this.state
    return Promise.all([
      TI.bbCheck(20, 2, exchangeId, tick, '3m', true),
      TI.bb(20, 2, 'close', exchangeId, tick, '3m', true)
    ])
      .then(([bbCheckRes, bollList]) => {
        const { upper: bollUp, middle: bollMiddle, lower: bollDown } = bollList[
          bollList.length - 1
        ]
        const { breakOut, direction, price } = bbCheckRes
        const lastBollItem = bollList[bollList.length - 2]
        const bollWidth = Decimal(bollUp)
          .sub(bollDown)
          .div(2)
          .toNumber()
          .toFixed()
        this.state.shakeBuyOrder = service.getOpenOrderInfo({
          lastBollItem,
          bollUp,
          price,
          bollDown,
          bollWidth,
          orderDirection: 'buy'
        })
        this.state.shakeSellOrder = service.getOpenOrderInfo({
          lastBollItem,
          bollUp,
          price,
          bollDown,
          bollWidth,
          orderDirection: 'sell'
        })
        return {
          // 是否上穿布林线
          isBreakUpBB: breakOut && direction === 'up',
          // 是否下穿布林线
          isBreakDownBB: breakOut && direction === 'down',
          // 当前价格
          price,
          // 布林半径
          bollWidth,
          bollUp,
          bollMiddle,
          bollDown,
          lastBollItem
        }
      })
      .catch(err => {
        console.log(`【${exchangeId}】获取是否怼穿了布林线判断失败`)
        console.log(err)
      })
  }

  _requestJudgeStochRSI () {
    const { exchangeId, tick } = this.state
    return TI.stochasticrsi(
      3,
      3,
      14,
      14,
      'close',
      exchangeId,
      tick,
      '3m',
      true
    ).then(stochRSIList => {
      const { stochRSI, d } = stochRSIList[stochRSIList.length - 1]
      return {
        stochRSI,
        d
      }
    })
  }

  // 撤单
  _cancelAllOrders (params) {
    const { tick } = this.state
    this.$exchange.cancelAllOrders(tick).then(({ code }) => {
      if (code === 200 && params) {
        const {
          orderOpenPrice,
          stopLossPrice,
          takeProfitPrice,
          orderDirection
        } = params
        Service.formatOrderLog({
          title:
            orderDirection === OrderDirection.BUY
              ? '请求多单撤单'
              : '请求多单撤单',
          type: LogType.ORDER,
          orderOpenPrice,
          stopLossPrice,
          takeProfitPrice
        })
      }
    })
  }
  // 下单
  _createOrder ({
    freeAmount,
    price,
    orderDirection,
    orderOpenPrice,
    stopLossPrice,
    takeProfitPrice
  }) {
    const { tick, levarage, buildRatio } = this.state
    const usdtQuantity = Decimal(levarage)
      .mul(freeAmount)
      .mul(buildRatio)
      .toNumber()

    const btcQuantity = Decimal(usdtQuantity)
      .div(price)
      .toNumber()

    const otherSide = OrderDirection.getOtherSide(orderDirection)

    return Promise.all([
      this.$exchange.createOrder(
        tick,
        'LIMIT',
        orderDirection,
        btcQuantity,
        orderOpenPrice,
        {
          positionSide: PositionSide[orderDirection]
        }
      ),
      this.$exchange.createOrder(
        tick,
        'STOP_MARKET',
        otherSide,
        btcQuantity,
        stopLossPrice,
        {
          positionSide: PositionSide[orderDirection],
          stopPrice: stopLossPrice,
          closePosition: true
        }
      ),
      this.$exchange.createOrder(
        tick,
        'TAKE_PROFIT_MARKET',
        otherSide,
        btcQuantity,
        takeProfitPrice,
        {
          positionSide: PositionSide[orderDirection],
          stopPrice: takeProfitPrice,
          closePosition: true
        }
      )
    ])
      .then(() => {
        return this._requestBalance().then(({ isFree }) => {
          if (!isFree) {
            Service.formatOrderLog({
              title: `挂${OrderDirectionDesc[orderDirection]}成功`,
              type: LogType.ORDER,
              orderOpenPrice,
              stopLossPrice,
              takeProfitPrice
            })
          } else {
            Service.formatOrderLog({
              title: `挂${OrderDirectionDesc[orderDirection]}失败`,
              type: LogType.ORDER,
              orderOpenPrice,
              stopLossPrice,
              takeProfitPrice
            })
            this.setStore(OPEN_ORDER_INFO, null)
          }
        })
      })
      .catch(err => {
        console.warn(err)
        this.setStore(OPEN_ORDER_INFO, null)
      })
  }
  setStore (key, data) {
    service.setJson(key, data || undefined)
    this.state[key] = data
  }
  getStore (key) {
    this.state[key] = service.getJson(key)
    return this.state[key]
  }
  _isSufferingLastLoss ({ lastLossInfo, bollMiddle, price }) {
    if (!lastLossInfo) return false
    const {
      // lastLostTimeStamp,
      orderDirection
    } = lastLossInfo
    // const lastLossMoment = moment(lastLostTimeStamp)
    // 曾触发多单止损，现在需要重新越过布林中轨（价格大于中线），才回归正常监听
    if (orderDirection === OrderDirection.BUY) {
      price >= bollMiddle
      this.setStore(LAST_LOSS_INFO, null)
      Service.formatLog({
        title: '自上一次多单止损，价格重新越过布林线，重启监听',
        type: LogType.MAIN
      })
    }

    // 曾触发空单止损，现在需要重新越过布林中轨（价格小于中线），才回归正常监听
    if (orderDirection === OrderDirection.BUY) {
      price <= bollMiddle
      this.setStore(LAST_LOSS_INFO, null)
      Service.formatLog({
        title: '自上一次空单止损，价格重新越过布林线，重启监听',
        type: LogType.MAIN
      })
    }
  }
  _isBollWidthNormal ({ bollWidth, price, minBollWidthRatio }) {
    const isWidth = bollWidth >= this._getMinBollWidth(price, minBollWidthRatio)
    return isWidth
  }
  _getMinBollWidth (price, minBollWidthRatio) {
    return Decimal(price)
      .mul(minBollWidthRatio)
      .toNumber()
  }
  _getBollStatus ({
    bollWidth,
    bollUp,
    bollMiddle,
    bollDown,
    price,
    bollStatusList: _bollStatusList = []
  }) {
    const { minBollWidthRatio } = this.state
    // 定义布林项状态
    let bollStatus = BollStatus.SHAKE
    let bollStatusList = _bollStatusList
    // 判断是否过窄
    const isNarrow = !this._isBollWidthNormal({
      bollWidth,
      price,
      minBollWidthRatio
    })
    // 判断布林状态是否有效
    const lastBollStatusItem = bollStatusList[bollStatusList.length - 1] || {}
    const isBollStatusListValid =
      lastBollStatusItem &&
      moment(lastBollStatusItem.timeStamp).isSameOrAfter(
        moment().add(-10, 'minutes')
      )
    const bollWidthDiff = _.get(this.state, 'shakeSellOrder.bollWidthDiff')
    const bollUpDiff = _.get(this.state, 'shakeSellOrder.bollUpDiff')
    const bollDownDiff = _.get(this.state, 'shakeSellOrder.bollDownDiff')
    if (isNarrow) {
      // 布林带过窄
      bollStatus = BollStatus.NARROW
    } else {
      // 上一项距离当前10分钟内，则上以项有效
      if (isBollStatusListValid) {
        switch (lastBollStatusItem.bollStatus) {
          // 前面过窄，则目前为单边
          case BollStatus.NARROW:
            bollStatus =
              price >
              Decimal(bollUp)
                .add(bollWidth * 0.2)
                .toNumber()
                ? BollStatus.ONESIDEUP
                : BollStatus.ONESIDEDOWN
            break
          // 前面如果为单边，则判断布林半径是否变窄以及布林轨变为负数
          case BollStatus.ONESIDEUP:
            bollStatus =
              bollWidthDiff < 0 && bollUpDiff < 0
                ? BollStatus.SHAKE
                : BollStatus.ONESIDEUP
            break
          case BollStatus.ONESIDEDOWN:
            bollStatus =
              bollWidthDiff < 0 && bollDownDiff < 0
                ? BollStatus.SHAKE
                : BollStatus.ONESIDEDOWN
            break
          // 本来是震荡，如果突出布林线外半径的40%，认为进入单边
          case BollStatus.NORMAL:
            if (
              price >
              Decimal(bollUp)
                .add(bollWidth * 0.4)
                .toNumber()
            ) {
              bollStatus = BollStatus.ONESIDEUP
            }

            if (
              price <
              Decimal(bollDown)
                .sub(bollWidth * 0.4)
                .toNumber()
            ) {
              bollStatus = BollStatus.ONESIDEDOWN
            }
            break
          // 默认是震荡行情
          default:
            bollStatus = lastBollStatusItem.bollStatus
            break
        }
      } else {
        bollStatusList = []
      }
      // 如果前20项都是布林带过窄，并且上一项
    }
    // 维护布林列表状态，存入新状态
    const bollStatusItem = {
      price,
      bollUp,
      bollMiddle,
      bollWidth,
      bollDown,
      timeStamp: moment().valueOf(),
      timeFormated: moment().format(),
      minBollWidthRatio,
      minBollWidth: this._getMinBollWidth(price, minBollWidthRatio),
      bollStatus
    }
    // 只存100项
    const newBollStatusList = [
      ...bollStatusList
        .filter(item => item.minBollWidthRatio === minBollWidthRatio)
        .slice(-100),
      bollStatusItem
    ]
    this.setStore(BOLL_STATUS_LIST, {
      list: newBollStatusList
    })
    this._onStatusChange(lastBollStatusItem.bollStatus, bollStatus)
    service.addLogParams({ bollStatus })
    return bollStatus
  }
  _onStatusChange (from, to) {
    const title = BollStatusChangeDesc[`${from}_TO_${to}`]
    Service.formatLog({
      title,
      type: LogType.MAIN
    })
  }
}

module.exports = AutoTrader
